/*-------------------------------------------------------------------------
 *
 * bernoulli.c
 *	  支持 BERNOULLI 表样本方法的例程
 *
 * 为了确保样本的重复性，选择给定元组的方式必须与历史无关；否则，同步扫描将打破重复性，更不用说诸如物理扩展或缩短关系等逻辑无关的维护。
 *
 * 为了实现这一点，我们通过将每个候选 TID 与活动种子进行哈希处理，然后选择它，如果哈希值小于由 BeginSampleScan 计算的选择概率的截止值。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/access/tablesample/bernoulli.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <math.h>

#include "access/tsmapi.h"
#include "catalog/pg_type.h"
#include "common/hashfn.h"
#include "optimizer/optimizer.h"
#include "utils/builtins.h"


/* 私有状态 */
typedef struct
{
	uint64		cutoff;			/* 选择哈希值小于此的元组 */
	uint32		seed;			/* 随机种子 */
	OffsetNumber lt;			/* 从当前块返回的最后一个元组 */
} BernoulliSamplerData;


static void fc_bernoulli_samplescangetsamplesize(PlannerInfo *fc_root,
											  RelOptInfo *fc_baserel,
											  List *fc_paramexprs,
											  BlockNumber *fc_pages,
											  double *fc_tuples);
static void fc_bernoulli_initsamplescan(SampleScanState *fc_node,
									 int fc_eflags);
static void fc_bernoulli_beginsamplescan(SampleScanState *fc_node,
									  Datum *fc_params,
									  int fc_nparams,
									  uint32 fc_seed);
static OffsetNumber fc_bernoulli_nextsampletuple(SampleScanState *fc_node,
											  BlockNumber fc_blockno,
											  OffsetNumber fc_maxoffset);


/*
 * 为BERNOULLI方法创建TsmRoutine描述符。
 */
Datum tsm_bernoulli_handler(PG_FUNCTION_ARGS)
{
	TsmRoutine *fc_tsm = makeNode(TsmRoutine);

	fc_tsm->parameterTypes = list_make1_oid(FLOAT4OID);
	fc_tsm->repeatable_across_queries = true;
	fc_tsm->repeatable_across_scans = true;
	fc_tsm->SampleScanGetSampleSize = fc_bernoulli_samplescangetsamplesize;
	fc_tsm->InitSampleScan = fc_bernoulli_initsamplescan;
	fc_tsm->BeginSampleScan = fc_bernoulli_beginsamplescan;
	fc_tsm->NextSampleBlock = NULL;
	fc_tsm->NextSampleTuple = fc_bernoulli_nextsampletuple;
	fc_tsm->EndSampleScan = NULL;

	PG_RETURN_POINTER(fc_tsm);
}

/*
 * 样本大小估计。
 */
static void fc_bernoulli_samplescangetsamplesize(PlannerInfo *fc_root,
								  RelOptInfo *fc_baserel,
								  List *fc_paramexprs,
								  BlockNumber *fc_pages,
								  double *fc_tuples)
{
	Node	   *fc_pctnode;
	float4		fc_samplefract;

	/* 尝试提取样本百分比的估计值 */
	fc_pctnode = (Node *) linitial(fc_paramexprs);
	fc_pctnode = estimate_expression_value(fc_root, fc_pctnode);

	if (IsA(fc_pctnode, Const) &&
		!((Const *) fc_pctnode)->constisnull)
	{
		fc_samplefract = DatumGetFloat4(((Const *) fc_pctnode)->constvalue);
		if (fc_samplefract >= 0 && fc_samplefract <= 100 && !isnan(fc_samplefract))
			fc_samplefract /= 100.0f;
		else
		{
			/* 如果值无效，使用默认的samplefract */
			fc_samplefract = 0.1f;
		}
	}
	else
	{
		/* 如果没有获得非空的Const，则使用默认的samplefract */
		fc_samplefract = 0.1f;
	}

	/* 我们将访问baserel的所有页面 */
	*fc_pages = fc_baserel->pages;

	*fc_tuples = clamp_row_est(fc_baserel->tuples * fc_samplefract);
}

/*
 * 在执行器设置期间初始化。
 */
static void fc_bernoulli_initsamplescan(SampleScanState *fc_node, int fc_eflags)
{
	fc_node->tsm_state = palloc0(sizeof(BernoulliSamplerData));
}

/*
 * 检查参数并准备进行样本扫描。
 */
static void fc_bernoulli_beginsamplescan(SampleScanState *fc_node,
						  Datum *fc_params,
						  int fc_nparams,
						  uint32 fc_seed)
{
	BernoulliSamplerData *fc_sampler = (BernoulliSamplerData *) fc_node->tsm_state;
	double		fc_percent = DatumGetFloat4(fc_params[0]);
	double		fc_dcutoff;

	if (fc_percent < 0 || fc_percent > 100 || isnan(fc_percent))
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_TABLESAMPLE_ARGUMENT),
				 errmsg("sample percentage must be between 0 and 100")));

	/*
	 * 截止值是样本概率乘以(PG_UINT32_MAX + 1)；我们必须
	 * 将其存储为uint64，当然。请注意，在零或一的概率极限处，这提供了严格
	 * 正确的行为。
	 */
	fc_dcutoff = rint(((double) PG_UINT32_MAX + 1) * fc_percent / 100);
	fc_sampler->cutoff = (uint64) fc_dcutoff;
	fc_sampler->seed = fc_seed;
	fc_sampler->lt = InvalidOffsetNumber;

	/*
	 * 使用bulkread，因为我们正在扫描所有页面。但在
	 * 较大采样比例时，pagemode可见性检查才有优势。这里的25%截止值
	 * 基于非常有限的实验。
	 */
	fc_node->use_bulkread = true;
	fc_node->use_pagemode = (fc_percent >= 25);
}

/*
 * 选择当前块中的下一个采样元组。
 *
 * 在这里返回一个偏移量而不知道元组是否可见
 * （甚至是否存在）是可以的。原因在于，我们对表中每个元组
 * 偏移量进行抛硬币操作。由于所有元组具有相同的返回概率，
 * 即使对不可见元组进行额外的抛硬币也无关紧要。
 *
 * 当我们到达块的末尾时，返回InvalidOffsetNumber，告诉
 * SampleScan转到下一个块。
 */
static OffsetNumber fc_bernoulli_nextsampletuple(SampleScanState *fc_node,
						  BlockNumber fc_blockno,
						  OffsetNumber fc_maxoffset)
{
	BernoulliSamplerData *fc_sampler = (BernoulliSamplerData *) fc_node->tsm_state;
	OffsetNumber fc_tupoffset = fc_sampler->lt;
	uint32		fc_hashinput[3];

	/* 前进到块中的第一个/下一个元组 */
	if (fc_tupoffset == InvalidOffsetNumber)
		fc_tupoffset = FirstOffsetNumber;
	else
		fc_tupoffset++;

	/*
	 * 我们通过对包含块、偏移量和种子的3个uint32数组应用hash_any来计算哈希。
	 * 这在设置上是高效的，并且根据当前的hash_any实现，它提供了
	 * 机器独立的结果，这是回归测试的一个不错特性。
	 *
	 * 哈希输入中的这些词在整个块中是相同的：
	 */
	fc_hashinput[0] = fc_blockno;
	fc_hashinput[2] = fc_sampler->seed;

	/*
	 * 循环遍历元组偏移量，直至找到合适的TID或到达块的末尾。
	 */
	for (; fc_tupoffset <= fc_maxoffset; fc_tupoffset++)
	{
		uint32		fc_hash;

		fc_hashinput[1] = fc_tupoffset;

		fc_hash = DatumGetUInt32(hash_any((const unsigned char *) fc_hashinput,
									   (int) sizeof(fc_hashinput)));
		if (fc_hash < fc_sampler->cutoff)
			break;
	}

	if (fc_tupoffset > fc_maxoffset)
		fc_tupoffset = InvalidOffsetNumber;

	fc_sampler->lt = fc_tupoffset;

	return fc_tupoffset;
}
